@@ -22,6 +22,7 @@ gem 'json', '~> 1.8.1' |
||
22 | 22 |
gem 'jsonpath', '~> 0.5.3' |
23 | 23 |
gem 'twilio-ruby', '~> 3.11.5' |
24 | 24 |
gem 'ruby-growl', '~> 4.1.0' |
25 |
+gem 'liquid', '~> 2.6.1' |
|
25 | 26 |
|
26 | 27 |
gem 'delayed_job', '~> 4.0.0' |
27 | 28 |
gem 'delayed_job_active_record', '~> 4.0.0' |
@@ -148,6 +148,7 @@ GEM |
||
148 | 148 |
activesupport (>= 3.0.0) |
149 | 149 |
kramdown (1.3.3) |
150 | 150 |
libv8 (3.16.14.3) |
151 |
+ liquid (2.6.1) |
|
151 | 152 |
macaddr (1.7.1) |
152 | 153 |
systemu (~> 2.6.2) |
153 | 154 |
mail (2.5.4) |
@@ -337,6 +338,7 @@ DEPENDENCIES |
||
337 | 338 |
jsonpath (~> 0.5.3) |
338 | 339 |
kaminari (~> 0.15.1) |
339 | 340 |
kramdown (~> 1.3.3) |
341 |
+ liquid (~> 2.6.1) |
|
340 | 342 |
mysql2 (~> 0.3.15) |
341 | 343 |
nokogiri (~> 1.6.1) |
342 | 344 |
protected_attributes (~> 1.0.7) |
@@ -0,0 +1,27 @@ |
||
1 |
+module LiquidInterpolatable |
|
2 |
+ extend ActiveSupport::Concern |
|
3 |
+ |
|
4 |
+ def interpolate_options options, payload |
|
5 |
+ duped_options = options.dup.tap do |duped_options| |
|
6 |
+ duped_options.each_pair do |key, value| |
|
7 |
+ if value.class == String |
|
8 |
+ duped_options[key] = Liquid::Template.parse(value).render(payload) |
|
9 |
+ else |
|
10 |
+ duped_options[key] = value |
|
11 |
+ end |
|
12 |
+ end |
|
13 |
+ end |
|
14 |
+ duped_options |
|
15 |
+ end |
|
16 |
+ |
|
17 |
+ require 'uri' |
|
18 |
+ # Percent encoding for URI conforming to RFC 3986. |
|
19 |
+ # Ref: http://tools.ietf.org/html/rfc3986#page-12 |
|
20 |
+ module Huginn |
|
21 |
+ def uri_escape(string) |
|
22 |
+ CGI::escape string |
|
23 |
+ end |
|
24 |
+ end |
|
25 |
+ |
|
26 |
+ Liquid::Template.register_filter(LiquidInterpolatable::Huginn) |
|
27 |
+end |
@@ -1,5 +1,6 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class EventFormattingAgent < Agent |
3 |
+ include LiquidInterpolatable |
|
3 | 4 |
cannot_be_scheduled! |
4 | 5 |
|
5 | 6 |
description <<-MD |
@@ -24,11 +25,11 @@ module Agents |
||
24 | 25 |
You can use an Event Formatting Agent's `instructions` setting to do this in the following way: |
25 | 26 |
|
26 | 27 |
"instructions": { |
27 |
- "message": "Today's conditions look like <$.conditions> with a high temperature of <$.high.celsius> degrees Celsius.", |
|
28 |
- "subject": "$.data" |
|
28 |
+ "message": "Today's conditions look like {{conditions}} with a high temperature of {{high.celsius}} degrees Celsius.", |
|
29 |
+ "subject": "{{data}}" |
|
29 | 30 |
} |
30 | 31 |
|
31 |
- JSONPaths must be between < and > . Make sure that you don't use these symbols anywhere else. |
|
32 |
+ FIXME Provide a link to a explanation on how to use liquid templating |
|
32 | 33 |
|
33 | 34 |
Events generated by this possible Event Formatting Agent will look like: |
34 | 35 |
|
@@ -60,18 +61,18 @@ module Agents |
||
60 | 61 |
So you can use it in `instructions` like this: |
61 | 62 |
|
62 | 63 |
"instructions": { |
63 |
- "message": "Today's conditions look like <$.conditions> with a high temperature of <$.high.celsius> degrees Celsius according to the forecast at <$.pretty_date.time>.", |
|
64 |
- "subject": "$.data" |
|
64 |
+ "message": "Today's conditions look like <$.conditions> with a high temperature of {{high.celsius}} degrees Celsius according to the forecast at {{pretty_date.time}}.", |
|
65 |
+ "subject": "{{data}}" |
|
65 | 66 |
} |
66 | 67 |
|
67 | 68 |
If you want to retain original contents of events and only add new keys, then set `mode` to `merge`, otherwise set it to `clean`. |
68 | 69 |
|
69 | 70 |
By default, the output event will have `agent` and `created_at` fields added as well, reflecting the original Agent type and Event creation time. You can skip these outputs by setting `skip_agent` and `skip_created_at` to `true`. |
70 | 71 |
|
71 |
- To CGI escape output (for example when creating a link), prefix with `escape`, like so: |
|
72 |
+ To CGI escape output (for example when creating a link), use the Liquid `uri_escape` filter, like so: |
|
72 | 73 |
|
73 | 74 |
{ |
74 |
- "message": "A peak was on Twitter in <$.group_by>. Search: https://twitter.com/search?q=<escape $.group_by>" |
|
75 |
+ "message": "A peak was on Twitter in {{group_by}}. Search: https://twitter.com/search?q={{group_by | uri_escape}}" |
|
75 | 76 |
} |
76 | 77 |
MD |
77 | 78 |
|
@@ -88,8 +89,8 @@ module Agents |
||
88 | 89 |
def default_options |
89 | 90 |
{ |
90 | 91 |
'instructions' => { |
91 |
- 'message' => "You received a text <$.text> from <$.fields.from>", |
|
92 |
- 'some_other_field' => "Looks like the weather is going to be <$.fields.weather>" |
|
92 |
+ 'message' => "You received a text {{text}} from {{fields.from}}", |
|
93 |
+ 'some_other_field' => "Looks like the weather is going to be {{fields.weather}}" |
|
93 | 94 |
}, |
94 | 95 |
'matchers' => [], |
95 | 96 |
'mode' => "clean", |
@@ -106,7 +107,7 @@ module Agents |
||
106 | 107 |
incoming_events.each do |event| |
107 | 108 |
formatted_event = options['mode'].to_s == "merge" ? event.payload.dup : {} |
108 | 109 |
payload = perform_matching(event.payload) |
109 |
- options['instructions'].each_pair {|key, value| formatted_event[key] = Utils.interpolate_jsonpaths(value, payload) } |
|
110 |
+ formatted_event.merge! interpolate_options(options['instructions'], payload) |
|
110 | 111 |
formatted_event['agent'] = Agent.find(event.agent_id).type.slice!(8..-1) unless options['skip_agent'].to_s == "true" |
111 | 112 |
formatted_event['created_at'] = event.created_at unless options['skip_created_at'].to_s == "true" |
112 | 113 |
create_event :payload => formatted_event |
@@ -1,6 +1,6 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class HipchatAgent < Agent |
3 |
- include JsonPathOptionsOverwritable |
|
3 |
+ include LiquidInterpolatable |
|
4 | 4 |
|
5 | 5 |
cannot_be_scheduled! |
6 | 6 |
cannot_create_events! |
@@ -18,22 +18,17 @@ module Agents |
||
18 | 18 |
If you want your message to notify the room members change `notify` to "true". |
19 | 19 |
Modify the background color of your message via the `color` attribute (one of "yellow", "red", "green", "purple", "gray", or "random") |
20 | 20 |
|
21 |
- If you want to specify either of those attributes per event, you can provide a [JSONPath](http://goessner.net/articles/JsonPath/) for each of them (except the `auth_token`). |
|
21 |
+ TODO: add a link to the wiki explaining how to use the Liquid templating |
|
22 | 22 |
MD |
23 | 23 |
|
24 | 24 |
def default_options |
25 | 25 |
{ |
26 | 26 |
'auth_token' => '', |
27 | 27 |
'room_name' => '', |
28 |
- 'room_name_path' => '', |
|
29 | 28 |
'username' => "Huginn", |
30 |
- 'username_path' => '', |
|
31 | 29 |
'message' => "Hello from Huginn!", |
32 |
- 'message_path' => '', |
|
33 | 30 |
'notify' => false, |
34 |
- 'notify_path' => '', |
|
35 | 31 |
'color' => 'yellow', |
36 |
- 'color_path' => '', |
|
37 | 32 |
} |
38 | 33 |
end |
39 | 34 |
|
@@ -49,14 +44,9 @@ module Agents |
||
49 | 44 |
def receive(incoming_events) |
50 | 45 |
client = HipChat::Client.new(options[:auth_token]) |
51 | 46 |
incoming_events.each do |event| |
52 |
- mo = merge_json_path_options event |
|
47 |
+ mo = interpolate_options options, event.payload |
|
53 | 48 |
client[mo[:room_name]].send(mo[:username], mo[:message], :notify => mo[:notify].to_s == 'true' ? 1 : 0, :color => mo[:color]) |
54 | 49 |
end |
55 | 50 |
end |
56 |
- |
|
57 |
- private |
|
58 |
- def options_with_path |
|
59 |
- [:room_name, :username, :message, :notify, :color] |
|
60 |
- end |
|
61 | 51 |
end |
62 | 52 |
end |
@@ -0,0 +1,11 @@ |
||
1 |
+class MigrateHipchatAndEfAgentToLiquid < ActiveRecord::Migration |
|
2 |
+ def change |
|
3 |
+ Agent.where(:type => 'Agents::HipchatAgent').each do |agent| |
|
4 |
+ LiquidMigrator.convert_all_agent_options(agent) |
|
5 |
+ end |
|
6 |
+ Agent.where(:type => 'Agents::EventFormattingAgent').each do |agent| |
|
7 |
+ agent.options['instructions'] = LiquidMigrator.convert_hash(agent.options['instructions'], {:merge_path_attributes => true, :leading_dollarsign_is_jsonpath => true}) |
|
8 |
+ agent.save |
|
9 |
+ end |
|
10 |
+ end |
|
11 |
+end |
@@ -0,0 +1,57 @@ |
||
1 |
+module LiquidMigrator |
|
2 |
+ def self.convert_all_agent_options(agent) |
|
3 |
+ agent.options = self.convert_hash(agent.options, {:merge_path_attributes => true, :leading_dollarsign_is_jsonpath => true}) |
|
4 |
+ agent.save! |
|
5 |
+ end |
|
6 |
+ |
|
7 |
+ def self.convert_hash(hash, options={}) |
|
8 |
+ options = {:merge_path_attributes => false, :leading_dollarsign_is_jsonpath => false}.merge options |
|
9 |
+ keys_to_remove = [] |
|
10 |
+ hash.tap do |hash| |
|
11 |
+ hash.each_pair do |key, value| |
|
12 |
+ case value.class.to_s |
|
13 |
+ when 'String', 'FalseClass', 'TrueClass' |
|
14 |
+ path_key = "#{key}_path" |
|
15 |
+ if options[:merge_path_attributes] && !hash[path_key].nil? |
|
16 |
+ # replace the value if the path is present |
|
17 |
+ value = hash[path_key] if hash[path_key].present? |
|
18 |
+ # in any case delete the path attibute |
|
19 |
+ keys_to_remove << path_key |
|
20 |
+ end |
|
21 |
+ hash[key] = LiquidMigrator.convert_string value, options[:leading_dollarsign_is_jsonpath] |
|
22 |
+ when 'Hash' |
|
23 |
+ # might want to make it recursive? |
|
24 |
+ when 'Array' |
|
25 |
+ # do we need it? |
|
26 |
+ end |
|
27 |
+ end |
|
28 |
+ # remove the unneeded *_path attributes |
|
29 |
+ end.select { |k, v| !keys_to_remove.include? k } |
|
30 |
+ end |
|
31 |
+ |
|
32 |
+ def self.convert_string(string, leading_dollarsign_is_jsonpath=false) |
|
33 |
+ if string == true || string == false |
|
34 |
+ # there might be empty *_path attributes for boolean defaults |
|
35 |
+ string |
|
36 |
+ elsif string[0] == '$' && leading_dollarsign_is_jsonpath |
|
37 |
+ # in most cases a *_path attribute |
|
38 |
+ convert_json_path string |
|
39 |
+ else |
|
40 |
+ # migrate the old interpolation syntax to the new liquid based |
|
41 |
+ string.gsub(/<([^>]+)>/).each do |
|
42 |
+ match = $1 |
|
43 |
+ if match =~ /\Aescape / |
|
44 |
+ # convert the old escape syntax to a liquid filter |
|
45 |
+ self.convert_json_path(match.gsub(/\Aescape /, '').strip, ' | uri_escape') |
|
46 |
+ else |
|
47 |
+ self.convert_json_path(match.strip) |
|
48 |
+ end |
|
49 |
+ end |
|
50 |
+ end |
|
51 |
+ end |
|
52 |
+ |
|
53 |
+ def self.convert_json_path(string, filter = "") |
|
54 |
+ "{{#{string[2..-1].gsub(/\.\*\Z/, '')}#{filter}}}" |
|
55 |
+ end |
|
56 |
+end |
|
57 |
+ |
@@ -0,0 +1,73 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe LiquidMigrator do |
|
4 |
+ describe "converting JSONPath strings" do |
|
5 |
+ it "should work" do |
|
6 |
+ LiquidMigrator.convert_string("$.data", true).should == "{{data}}" |
|
7 |
+ LiquidMigrator.convert_string("$.data.test", true).should == "{{data.test}}" |
|
8 |
+ LiquidMigrator.convert_string("$.data.test.*", true).should == "{{data.test}}" |
|
9 |
+ end |
|
10 |
+ |
|
11 |
+ it "should ignore strings which just contain a JSONPath" do |
|
12 |
+ LiquidMigrator.convert_string("$.data").should == "$.data" |
|
13 |
+ LiquidMigrator.convert_string(" $.data", true).should == " $.data" |
|
14 |
+ LiquidMigrator.convert_string("lorem $.data", true).should == "lorem $.data" |
|
15 |
+ end |
|
16 |
+ end |
|
17 |
+ |
|
18 |
+ describe "converting escaped JSONPath strings" do |
|
19 |
+ it "should work" do |
|
20 |
+ LiquidMigrator.convert_string("Received <$.content.text.*> from <$.content.name> .").should == |
|
21 |
+ "Received {{content.text}} from {{content.name}} ." |
|
22 |
+ LiquidMigrator.convert_string("Weather looks like <$.conditions> according to the forecast at <$.pretty_date.time>").should == |
|
23 |
+ "Weather looks like {{conditions}} according to the forecast at {{pretty_date.time}}" |
|
24 |
+ end |
|
25 |
+ |
|
26 |
+ it "should convert the 'escape' method correctly" do |
|
27 |
+ LiquidMigrator.convert_string("Escaped: <escape $.content.name>\nNot escaped: <$.content.name>").should == |
|
28 |
+ "Escaped: {{content.name | uri_escape}}\nNot escaped: {{content.name}}" |
|
29 |
+ end |
|
30 |
+ end |
|
31 |
+ |
|
32 |
+ describe "migrating a hash" do |
|
33 |
+ it "should convert every attribute" do |
|
34 |
+ LiquidMigrator.convert_hash({'a' => "$.data", 'b' => "This is a <$.test>"}).should == |
|
35 |
+ {'a' => "$.data", 'b' => "This is a {{test}}"} |
|
36 |
+ end |
|
37 |
+ it "should work with leading_dollarsign_is_jsonpath" do |
|
38 |
+ LiquidMigrator.convert_hash({'a' => "$.data", 'b' => "This is a <$.test>"}, leading_dollarsign_is_jsonpath: true).should == |
|
39 |
+ {'a' => "{{data}}", 'b' => "This is a {{test}}"} |
|
40 |
+ end |
|
41 |
+ it "should use the corresponding *_path attributes when using merge_path_attributes"do |
|
42 |
+ LiquidMigrator.convert_hash({'a' => "default", 'a_path' => "$.data"}, {leading_dollarsign_is_jsonpath: true, merge_path_attributes: true}).should == |
|
43 |
+ {'a' => "{{data}}"} |
|
44 |
+ end |
|
45 |
+ end |
|
46 |
+ |
|
47 |
+ describe "migrating an actual agent" do |
|
48 |
+ before do |
|
49 |
+ valid_params = { |
|
50 |
+ 'auth_token' => 'token', |
|
51 |
+ 'room_name' => 'test', |
|
52 |
+ 'room_name_path' => '', |
|
53 |
+ 'username' => "Huginn", |
|
54 |
+ 'username_path' => '$.username', |
|
55 |
+ 'message' => "Hello from Huginn!", |
|
56 |
+ 'message_path' => '$.message', |
|
57 |
+ 'notify' => false, |
|
58 |
+ 'notify_path' => '', |
|
59 |
+ 'color' => 'yellow', |
|
60 |
+ 'color_path' => '', |
|
61 |
+ } |
|
62 |
+ |
|
63 |
+ @agent = Agents::HipchatAgent.new(:name => "somename", :options => valid_params) |
|
64 |
+ @agent.user = users(:jane) |
|
65 |
+ @agent.save! |
|
66 |
+ end |
|
67 |
+ |
|
68 |
+ it "should work" do |
|
69 |
+ LiquidMigrator.convert_all_agent_options(@agent) |
|
70 |
+ @agent.reload.options.should == {"auth_token" => 'token', 'color' => 'yellow', 'notify' => false, 'room_name' => 'test', 'username' => '{{username}}', 'message' => '{{message}}'} |
|
71 |
+ end |
|
72 |
+ end |
|
73 |
+end |
@@ -6,8 +6,8 @@ describe Agents::EventFormattingAgent do |
||
6 | 6 |
:name => "somename", |
7 | 7 |
:options => { |
8 | 8 |
:instructions => { |
9 |
- :message => "Received <$.content.text.*> from <$.content.name> .", |
|
10 |
- :subject => "Weather looks like <$.conditions> according to the forecast at <$.pretty_date.time>" |
|
9 |
+ :message => "Received {{content.text}} from {{content.name}} .", |
|
10 |
+ :subject => "Weather looks like {{conditions}} according to the forecast at {{pretty_date.time}}" |
|
11 | 11 |
}, |
12 | 12 |
:mode => "clean", |
13 | 13 |
:matchers => [ |
@@ -82,7 +82,7 @@ describe Agents::EventFormattingAgent do |
||
82 | 82 |
it "should allow escaping" do |
83 | 83 |
@event.payload[:content][:name] = "escape this!?" |
84 | 84 |
@event.save! |
85 |
- @checker.options[:instructions][:message] = "Escaped: <escape $.content.name>\nNot escaped: <$.content.name>" |
|
85 |
+ @checker.options[:instructions][:message] = "Escaped: {{content.name | uri_escape}}\nNot escaped: {{content.name}}" |
|
86 | 86 |
@checker.save! |
87 | 87 |
@checker.receive([@event]) |
88 | 88 |
Event.last.payload[:message].should == "Escaped: escape+this%21%3F\nNot escaped: escape this!?" |
@@ -1,22 +1,17 @@ |
||
1 | 1 |
require 'spec_helper' |
2 |
-require 'models/concerns/json_path_options_overwritable' |
|
2 |
+require 'models/concerns/liquid_interpolatable' |
|
3 | 3 |
|
4 | 4 |
describe Agents::HipchatAgent do |
5 |
- it_behaves_like JsonPathOptionsOverwritable |
|
5 |
+ it_behaves_like LiquidInterpolatable |
|
6 | 6 |
|
7 | 7 |
before(:each) do |
8 | 8 |
@valid_params = { |
9 | 9 |
'auth_token' => 'token', |
10 | 10 |
'room_name' => 'test', |
11 |
- 'room_name_path' => '', |
|
12 |
- 'username' => "Huginn", |
|
13 |
- 'username_path' => '$.username', |
|
14 |
- 'message' => "Hello from Huginn!", |
|
15 |
- 'message_path' => '$.message', |
|
11 |
+ 'username' => "{{username}}", |
|
12 |
+ 'message' => "{{message}}", |
|
16 | 13 |
'notify' => false, |
17 |
- 'notify_path' => '', |
|
18 | 14 |
'color' => 'yellow', |
19 |
- 'color_path' => '', |
|
20 | 15 |
} |
21 | 16 |
|
22 | 17 |
@checker = Agents::HipchatAgent.new(:name => "somename", :options => @valid_params) |
@@ -0,0 +1,31 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+shared_examples_for LiquidInterpolatable do |
|
4 |
+ before(:each) do |
|
5 |
+ @valid_params = { |
|
6 |
+ "normal" => "just some normal text", |
|
7 |
+ "variable" => "{{variable}}", |
|
8 |
+ "text" => "Some test with an embedded {{variable}}", |
|
9 |
+ "escape" => "This should be {{hello_world | uri_escape}}" |
|
10 |
+ } |
|
11 |
+ |
|
12 |
+ @checker = described_class.new(:name => "somename", :options => @valid_params) |
|
13 |
+ @checker.user = users(:jane) |
|
14 |
+ |
|
15 |
+ @event = Event.new |
|
16 |
+ @event.agent = agents(:bob_weather_agent) |
|
17 |
+ @event.payload = { :variable => 'hello', :hello_world => "Hello world"} |
|
18 |
+ @event.save! |
|
19 |
+ end |
|
20 |
+ |
|
21 |
+ describe "interpolating liquid templates" do |
|
22 |
+ it "should work" do |
|
23 |
+ @checker.send(:interpolate_options, @checker.options, @event.payload).should == { |
|
24 |
+ "normal" => "just some normal text", |
|
25 |
+ "variable" => "hello", |
|
26 |
+ "text" => "Some test with an embedded hello", |
|
27 |
+ "escape" => "This should be Hello+world" |
|
28 |
+ } |
|
29 |
+ end |
|
30 |
+ end |
|
31 |
+end |